package org.bouncycastle.openssl.test;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.security.GeneralSecurityException;
import java.security.PublicKey;
import java.security.Security;
import java.util.*;

import javax.annotation.Generated;
import javax.crypto.Cipher;

import org.bouncycastle.openssl.PEMParser;

public class MainClass {
	//private static final String INPUT_FILE_NAME = "transactionData-10000-3.bin";

	private static final int BLOCK_HEADER_SIZE = 82;
	private static final int BLOCK_HEADER_PREV_BLOCK_REF_OFFSET = 4;
	private static final int BLOCK_HEADER_MERKLE_ROOT_OFFSET = 36;		 
	private static final int BLOCK_HEADER_CREATION_TIME_OFFSET = 68;
	private static final int BLOCK_HEADER_DIFFICULTY_OFFSET = 72;
	private static final int BLOCK_HEADER_NONCE_OFFSET = 74;
	private static final int BLOCK_HEADER_VERSION_NUMBER = 1;
	private static final int BLOCK_HEADER_DIFFICULTY = 3;
	

	private static final int REGULAR_TRANSACTION_OFFSET = BLOCK_HEADER_SIZE + 4 + 2 + 2 + 
			36;
	
	private static final int MERKLE_NODE_DATA_SIZE = 32;
	
	private static final String MINER_PKEY = "-----BEGIN RSA PUBLIC KEY-----\n" +
			"MIGJAoGBAN3MxXHcbc1VNKTOgdm7W+i/dVnjv8vYGlbkdaTKzYgi8rQm126Sri87\n" +
			"702UBNzmkkZyKbRKL/Bfc4EG8/Mt9Pd2xQlRyXCL9FnIFWHyhfIQtW+oBsGI5UhG\n" +
			"I8B8MiPOMfb6d/PdK+vd4riUxHAvCkHW5Lw0szAD1RVGbkG/7qnzAgMBAAE=\n" +
			"-----END RSA PUBLIC KEY-----";
	
	private static final long VERIFICATION_AWARD_PER_BLOCK = 10L;		

	private static List<Transaction> transactions = new ArrayList<Transaction>();
	private static Map<String, Long> dHashToIndex = new HashMap<String, Long>();
	
	// stores the balance for each public key
	private static Map<String, Long> pKeyToBalance = new HashMap<String, Long>();

	 
	// adopted from http://www.javapractices.com/topic/TopicAction.do?Id=245
	/** Read the given binary file, and return its contents as a byte array.*/
	public static byte[] read(String aInputFileName){	    
		File file = new File(aInputFileName);
		byte[] result = new byte[(int)file.length()];
		try {
			InputStream input = null;
			try {
				int totalBytesRead = 0;
				input = new BufferedInputStream(new FileInputStream(file));
				while(totalBytesRead < result.length){
					int bytesRemaining = result.length - totalBytesRead;
					//input.read() returns -1, 0, or more :
					int bytesRead = input.read(result, totalBytesRead, bytesRemaining); 
					if (bytesRead > 0){
						totalBytesRead = totalBytesRead + bytesRead;
					}
				}		
			}
			finally {
				input.close();
			}
		}
		catch (Exception ex) {
			ex.printStackTrace();
		}
		return result;
	}


	/**
	 * @param args
	 * @throws IOException 
	 */
	public static void main(String[] args) throws IOException {
		System.out.println("Reading from file " + args[2] + "...");
		byte [] binaryData = read(args[2]);
		
		// opens the output file
		FileOutputStream outputStream = null;
		try {
			outputStream = new FileOutputStream(args[3]);
		} catch (FileNotFoundException e1) {
			e1.printStackTrace();
		}		
		
		// get the genesis block header
		byte [] genesisBlockHeader = Arrays.copyOfRange(binaryData, 0, BLOCK_HEADER_SIZE);
		
		// writes the genesis block header
		outputStream.write(genesisBlockHeader);
		
		byte [] genesisBlockTxnCount = Arrays.copyOfRange(binaryData, BLOCK_HEADER_SIZE, BLOCK_HEADER_SIZE + 4);
		
		// writes the genesis block transaction count
		outputStream.write(genesisBlockTxnCount);
		
		// instantiate the genesis transaction
		int curOffSet = BLOCK_HEADER_SIZE + 4;
		
		Transaction genesisTxn = new Transaction(binaryData, curOffSet, true);		
		curOffSet += genesisTxn.getDataSize();
		transactions.add(genesisTxn);
		
		try {
			dHashToIndex.put(new String(genesisTxn.getDHashedData(), "UTF-8"), 0L);
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		
		// write the genesis transaction
		outputStream.write(genesisTxn.getData());
		
		// adds the output of the genesis block to the recipients' balances
		for (int i = 0; i < genesisTxn.getNumOutputs(); i++) {
			byte [] recipientDHashedPKey = genesisTxn.getDHashedOutputPkey(i);				
			String recipientDHashedPKeyStr = Util.getPackedHexStringFromBytes(recipientDHashedPKey);

			long recipientBalance = pKeyToBalance.containsKey(recipientDHashedPKeyStr) ? 
					pKeyToBalance.get(recipientDHashedPKeyStr) :
						0;
			
			pKeyToBalance.put(recipientDHashedPKeyStr, recipientBalance + genesisTxn.getOutputValueAtIdx(i));
		}
				
		// instantiate the rest of the regular transactions
		long numRegularTransactions = Util.readLongLittleEndianFromFourBytes(binaryData,
				REGULAR_TRANSACTION_OFFSET);

		curOffSet = REGULAR_TRANSACTION_OFFSET + 4;

		// starting at index 1 because genesis transaction takes the 0th spot
		for (long i = 1; i < numRegularTransactions + 1; i++) {
			Transaction t = new Transaction(binaryData, curOffSet, false);
			curOffSet += t.getDataSize();
			transactions.add(t);
			
			try {
				dHashToIndex.put(new String(t.getDHashedData(), "UTF-8"), i);
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
			}
			
			if ((transactions.size()-1) % 1000 == 0)
				System.out.println("Finished constructing " + transactions.size() + " transactions");
		}
		
		System.out.println("Finished constructing all " + transactions.size() + " transactions");
		
		// get the valid transactions
		System.out.println("getting the valid transactions");
		List<Transaction> validTxns = getValidRegularTransactions(transactions);
		
		// collect the transaction fee award
		System.out.println("collecting the transaction fee award $$$$$");
		long transactionFeeAward = getTransactionFeeAward(validTxns); 
		
		// generate the coinbase transaction
		byte [] coinbaseTxn = generateCoinbaseTransaction(
				VERIFICATION_AWARD_PER_BLOCK + transactionFeeAward, MINER_PKEY);
				
		// add the coinbase transaction to the front of the list
		Transaction coinbase = new Transaction(coinbaseTxn, 0, true);
		if (!coinbase.checkValid()) {
			throw new IllegalStateException("generated coinbase is not valid");
		}
		
		validTxns.add(0, coinbase);
		
		// calculate the final balance for each pKey
		for (Transaction t : validTxns) {
			if (!t.isGenesisOrCoinBase()) {
				// deduct the totalOutput from the redeemer's balance (if there is a redeemer at all)
				byte [] dHashedRedeemerPKey = Util.dHash(t.getRedeemerPKey());
				String dHashedRedeemerPKeyStr = Util.getPackedHexStringFromBytes(dHashedRedeemerPKey);
				
				long redeemerBalance = pKeyToBalance.containsKey(dHashedRedeemerPKeyStr) ? 
						pKeyToBalance.get(dHashedRedeemerPKeyStr) :
							0L;
				
				// total deduction = output + surplus (surplus being collected by the miner)
				pKeyToBalance.put(dHashedRedeemerPKeyStr,
						redeemerBalance - (t.getTotalOutput() + t.getSurplus()));
			}
			
			for (int i = 0; i < t.getNumOutputs(); i++) {
				byte [] recipientDHashedPKey = t.getDHashedOutputPkey(i);				
				String recipientDHashedPKeyStr = Util.getPackedHexStringFromBytes(recipientDHashedPKey);

				long recipientBalance = pKeyToBalance.containsKey(recipientDHashedPKeyStr) ? 
						pKeyToBalance.get(recipientDHashedPKeyStr) :
							0L;
				
				pKeyToBalance.put(recipientDHashedPKeyStr, recipientBalance + t.getOutputValueAtIdx(i));
			}	
		}
		

		// generate the transaction block header based on all the valid transactions
		byte [] regularTransactionHeader = generateBlockHeader(Util.dHash(genesisBlockHeader), validTxns);
		
		// write the transaction block header
		outputStream.write(regularTransactionHeader);
		
		// get the transaction count (including the coinbase but not the genesis)
		long nonGenesisTxnCount = validTxns.size();
		byte [] nonGenesisTxnCountBytes = new byte[4];
		
		// write the transaction count
		for (int i = 0;	i < 4; i++) {
			nonGenesisTxnCountBytes[i] = (byte) ((nonGenesisTxnCount >> (i*8)) & 0xff);
		}
		outputStream.write(nonGenesisTxnCountBytes);
				
		// write all the valid transactions (including the coinbase but not the genesis)
		System.out.println("writing all the valid transactions (including the coinbase)");
		for (int i = 0; i < validTxns.size(); i++) {
			outputStream.write(validTxns.get(i).getData());
		}
				
		// print balance to a new file
		PrintWriter balanceWriter = new PrintWriter("balances.txt", "UTF-8");
		
		System.out.println("Balance Ledger:");
		for (String key : pKeyToBalance.keySet()) {			
			balanceWriter.println(key + " " + pKeyToBalance.get(key));
			System.out.println(key + " " + pKeyToBalance.get(key));
		}
		
		balanceWriter.close();
		outputStream.close();
	}

	// returns a transaction with specified dHash data, or null if no such transaction exist
	public static Transaction getTransaction(byte[] dHashedData) {
		String dHashedDataStr = null;
		try {
			dHashedDataStr = new String(dHashedData, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			e.printStackTrace();
		}
		
		if (dHashToIndex.containsKey(dHashedDataStr)) {
			long idx = dHashToIndex.get(dHashedDataStr);
			// casting idx to int since it is unlikely that idx will exceed INT_MAX
			return transactions.get((int) idx);
		} else {
			return null;
		}
	}
	

	// returns a new list containing all valid regular (non-genesis and non-coinbase) transactions in the list
	private static List<Transaction> getValidRegularTransactions(List<Transaction> transactions) {
		List<Transaction> result = new ArrayList<Transaction>();
		List<Integer> invalidIdx = new ArrayList<Integer>();
		
		// genesis transactions or coinbase transactions will be checked, but will not
		// be included in the result
		for (int i = 0; i < transactions.size(); i++) {
			Transaction txn = transactions.get(i);
			
			if (txn.checkValid()) {
				//System.out.println("transaction " + i + "is valid");
				if (!txn.isGenesisOrCoinBase())
					result.add(txn);
			} else {
				//System.out.println("transaction " + i + "is not valid");
				invalidIdx.add(i);
			}
		}
		
		System.out.println("Invalid transaction indices:");
		System.out.println(invalidIdx);
		
		return result;
	}
		
	// returns the total awarded transaction fee from the regular (non-genesis and non-coinbase) transactions
	private static long getTransactionFeeAward(List<Transaction> transactions) {
		long result = 0L;
		for (int i = 0; i < transactions.size(); i++) {
			Transaction txn = transactions.get(i);
			
			if (!txn.isGenesisOrCoinBase())
				result += txn.getSurplus();
		}
		
		return result;		
	}
	
	// returns null if input is empty
	public static byte[] computeMerkleRoot(List<Transaction> transactions) {		
		if (transactions.isEmpty()) return null;
		
		List<byte[]> txnDHash = new ArrayList<byte[]>();
		
		// get all the dHashed transactions
		for (int i = 0; i < transactions.size(); i++) {
			txnDHash.add( transactions.get(i).getDHashedData() );
		}
		
		// number of nodes at the bottom of the tree
		int currentLevelNodes = txnDHash.size();
		
		while(currentLevelNodes > 1) {
			
			for (int i = 0; i < currentLevelNodes; i += 2) {
				// data in the left node
				byte [] left = txnDHash.get(i);
				
				// data in the right node
				byte [] right;
				
				// if there is an odd number of nodes at this level
				// repeat the last node
				if (i == currentLevelNodes - 1) {
					right = txnDHash.get(i);				
				} else {
					right = txnDHash.get(i + 1);
				}
				
				// concatenate left and right
				byte [] leftRight = new byte[left.length + right.length];
				for (int j = 0; j < leftRight.length; j++) {
					if (j < left.length)
						leftRight[j] = left[j];
					else
						leftRight[j] = right[j - left.length];
				}
				
				// set the parent to the dhash of the concatenation
				txnDHash.set(i / 2, Util.dHash(leftRight));
			}
			
			// going to one level above, number of nodes is halved
			currentLevelNodes = (currentLevelNodes + 1) / 2;
		}
		
		return txnDHash.get(0);
	}

	/**
	 * generate a block header for the specified the list of transactions
	 * @param dHashPrevBlockHeader
	 * @param txns
	 * @return
	 */
	private static byte[] generateBlockHeader(byte[] dHashPrevBlockHeader, List<Transaction> txns) {
		byte [] result = new byte[BLOCK_HEADER_SIZE];
		
		// write the version number
		result[0] = (byte) (BLOCK_HEADER_VERSION_NUMBER & 0xff);
		
		// write the dhased name of the previous block header
		for (int i = BLOCK_HEADER_PREV_BLOCK_REF_OFFSET; 
				i < BLOCK_HEADER_MERKLE_ROOT_OFFSET; i++) {
			result[i] = dHashPrevBlockHeader[i - BLOCK_HEADER_PREV_BLOCK_REF_OFFSET];
		}
		
		// write the merkle root
		byte[] merkleRoot = computeMerkleRoot(txns);
		for (int i = BLOCK_HEADER_MERKLE_ROOT_OFFSET; 
				i < BLOCK_HEADER_CREATION_TIME_OFFSET; i++) {
			result[i] = merkleRoot[i - BLOCK_HEADER_MERKLE_ROOT_OFFSET];
		}
		
		// write the current time
		long currentTime = System.currentTimeMillis() / 1000L;
		for (int i = BLOCK_HEADER_CREATION_TIME_OFFSET; 
				i < BLOCK_HEADER_DIFFICULTY_OFFSET; i++) {
			result[i] = (byte) ((currentTime >> (i*8)) & 0xff);
		}
		
		// write the difficulty level
		result[BLOCK_HEADER_DIFFICULTY_OFFSET] = (byte) (BLOCK_HEADER_DIFFICULTY & 0xff);
		result[BLOCK_HEADER_DIFFICULTY_OFFSET + 1] = (byte) ((BLOCK_HEADER_DIFFICULTY >> 8) & 0xff);
		
		System.out.println("Mining for nonce...");
		// mine for nonce
		mineForNonce(result, BLOCK_HEADER_DIFFICULTY);
		
		return result;
	}
	
	private static byte [] generateCoinbaseTransaction(long reward, String pKeyBase64) {
		 byte [] result = new byte[2 + 2 + 36];
		 
		 // write the number of outputs
		 result[2] = 0x01;
		 
		 // write the output value
		 for (int i = 4; i < 8; i++) {
			 result[i] = (byte) ((reward >> ((i-4)*8)) & 0xff);
		 }
		 
		 // write the output destination (miner's pkey)
		 byte [] pKey = pKeyBase64.getBytes();
		 byte [] pKeyDHashed = Util.dHash(pKey);
		 
		 for (int i = 8; i < 40; i++) {
			 result[i] = pKeyDHashed[i - 8];
		 }
		 
		 return result;
	}
	
	// fills in the nonce satisfying the difficulty level in the last 8 bytes of the specified array
	// modifies the last 8 bytes of input array
	private static void mineForNonce(byte[] blockHeader, int difficulty) {
		// dump the contents of blockHeader into a byte buffer
		ByteBuffer bb = ByteBuffer.wrap(blockHeader);
		
		for (long guess = Long.MIN_VALUE; guess < Long.MAX_VALUE; guess++) {
			// write the guessed nonce in the last 8 bytes of the buffer
			bb.putLong(blockHeader.length - 8, guess);
			
			// get the current dhashed header
			byte [] curDHash = Util.dHash(bb.array());
			
			// check if the dhash value satisfies the difficulty criterion
			boolean guessIsCorrect = true;
			
			for (int i = 0; i < difficulty; i++) {
				if (curDHash[i] != 0x00) {
					guessIsCorrect = false;
					break;
				}
			}
			
			if (guessIsCorrect)
				break;
		}
	}
}
